package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import com.hmdp.utils.UserHolder;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;
import static com.hmdp.utils.SystemConstants.USER_NICK_NAME_PREFIX;

/**
 * <p>
 * 服务实现类
 * </p>
 */
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    //下面这两行是老式的 构造方法注入redis核心类，其实可以@Autowired
    private final StringRedisTemplate stringRedisTemplate;
    public UserServiceImpl(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //一：session发送手机验证码登录
    //三：改造：用redis存储验证码，而不是session
    /**
     * 发送手机验证码
     * @param phone 手机号
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //校验手机号（是否13位，格式是否规范）
        /**
         * 工具类RegexUtils：    校验手机号格式是否正确
         */
        if (RegexUtils.isPhoneInvalid(phone)){
            //不符合返回错误信息
            return Result.fail("手机号格式错误");
        }

        /**
         * 工具类 RandomUtil：    生成随机数
         *///符合生成验证码存入session
//        String code = RandomUtil.randomNumbers(6);//生成6位随机数
//        session.setAttribute("code", code);
//        System.out.println("手机号校验成功，发送验证码成功："+code+"，存入session");
//        return Result.ok(code);


        //生成验证码，存储session或者redis中，等待登录时校验
        //TODO 改造：用redis存储验证码等待登陆时校验，而不是session，以规避session在多个tomcat中不能共享的问题
        String code = RandomUtil.randomNumbers(6);//生成6位随机数
        // 验证码存入redis，设置有效期为2分钟--key,value,过期时间，单位
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
        log.info("验证码："+code);
        return Result.ok(code);
    }

    //二：：登录功能--手机号注册后登录，携带手机号和验证码请求，后端从session或redis中取出验证码校验，并生产出token（如果使用session就不需要token）
    //三：改造：用redis存储验证码，而不是session
    /**
     * 登录功能
     * @param loginForm   DTO: 登录参数，包含手机号、验证码；或者手机号、密码
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //校验手机号格式和用户提交的验证码是否正确
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)){
            //不符合返回错误信息
            return Result.fail("手机号格式错误");
        }
        String password = loginForm.getPassword();
        if (password == null){
            //密码为空，验证码校验
            String code = loginForm.getCode();  //用户提交的验证码
//            String code1 = (String) session.getAttribute("code"); //session中注册时生成的验证码
            //三：改造：用redis存储验证码，而不是session，从redis中，根据手机号，取出验证码
            String code1 = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone); //根据key（手机号），从redis中取出验证码
            if (code == null || !code.equals(code1)){
                //验证码错误
                return Result.fail("验证码错误");
            }
        }
        //根据手机号查询用户表，用户是否存在？mybatisplus提供的方法:    query().eq("phone", phone).one();
        User user = query().eq("phone", phone).one();
        if (user == null){
            //用户不存在，创建新用户并保存
            // TODO创建一个新用户对象并保存到数据库中
            user = createUserWithPhone(phone);
        }
//        //将用户对象存入session中，此后的所有请求都会携带session中的user信息，因此不需要jwt令牌
//        session.setAttribute("user", user);


        //登录校验验证码通过后，查询出用户对象，将user对象存入redis，和一个token
        //TODO  改造：用redis存储用户信息，而不是session
        //随机生成token，作为登录令牌
        String token = UUID.randomUUID().toString(true); //true: 去掉-
        //将用户对象存入redis，设置有效期为30分钟
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);  //类型转换，规避敏感信息

        // TODO 将用户对象存入redis中,在redis中采用hash结构存储（opsForHash），key为token，value为以一个user对象转化为的map集合，，可选：使用一个自定义序列化器--BeanUtil.beanToMap(userDTO, new HashMap<>()--将user对象转换为map集合，存储在redis中，省空间
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,
                BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create()
                        .setIgnoreNullValue(true) //忽略空值
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())));  //将对象中的所有属性的值转换为字符串类型

        //设置redis中存储的token的有效期为30分钟--key，过期时间，单位
        stringRedisTemplate.expire(LOGIN_USER_KEY+token, LOGIN_USER_TTL, TimeUnit.MINUTES);
        //返回token，给前端
        return Result.ok(token);
    }


    //签到
    @Override
    public Result sign() {
        // 1.获取当前登录用户
        Long userId = UserHolder.getUser().getId();
        // 2.获取日期
        LocalDateTime now = LocalDateTime.now();
        // 3.拼接key
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));  //格式化日期，格式为：:yyyyMM
        String key = USER_SIGN_KEY + userId + keySuffix;
        // 4.获取今天是本月的第几天
        int dayOfMonth = now.getDayOfMonth();
        // 5.写入Redis SETBIT key offset 1  ---位图数据结构：key是用户id+格式化日期，offset是第几位(代表着这个月的第几天)，value是true即1，代表签到
        stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
        return Result.ok();
    }



    //统计当前用户的连续签到次数
    @Override
    public Result signCount() {
        // 1.获取当前登录用户
        Long userId = UserHolder.getUser().getId();
        // 2.获取日期
        LocalDateTime now = LocalDateTime.now();
        // 3.拼接key
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = USER_SIGN_KEY + userId + keySuffix;
        // 4.获取今天是本月的第几天
        int dayOfMonth = now.getDayOfMonth();
        // 5.获取本月截止今天为止的所有的签到记录，返回的是一个十进制的数字 BITFIELD sign:5:202203 GET u14 0
        List<Long> result = stringRedisTemplate.opsForValue().bitField(
                key,
                BitFieldSubCommands.create()
                        .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
        );
        if (result == null || result.isEmpty()) {
            // 没有任何签到结果--今天没有签到过--直接返回0
            return Result.ok(0);
        }
        Long num = result.get(0);
        if (num == null || num == 0) {
            return Result.ok(0);
        }
        // 6.循环遍历
        int count = 0;
        while (true) {
            // 6.1.让这个数字与1做与运算，得到数字的最后一个bit位  // 判断这个bit位是否为0
            if ((num & 1) == 0) {
                // 如果为0，说明未签到，结束
                break;
            }else {
                // 如果不为0，说明已签到，计数器+1
                count++;
            }
            // 把数字右移一位，抛弃最后一个bit位，继续下一个bit位
            num >>>= 1;  //无符号右移？？？
        }
        return Result.ok(count);
    }





    //创建一个用户对象并存入数·据库
    private User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomNumbers(6));//常量+随机生成6位昵称
        //保存用户，mybatisplus提供的方法:    save(user);
        save(user);
        return user;
    }
}
